<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="style-transformer.html">

<script>

  Polymer.StyleProperties = (function() {

    var nativeShadow = Polymer.Settings.useNativeShadow;
    var matchesSelector = Polymer.DomApi.matchesSelector;
    var styleUtil = Polymer.StyleUtil;
    var styleTransformer = Polymer.StyleTransformer;

    return {

      // decorates styles with rule info and returns an array of used style
      // property names
      decorateStyles: function(styles) {
        var self = this, props = {};
        styleUtil.forRulesInStyles(styles, function(rule) {
          self.decorateRule(rule);
          self.collectPropertiesInCssText(rule.propertyInfo.cssText, props);
        });
        // return this list of property names *consumes* in these styles.
        var names = [];
        for (var i in props) {
          names.push(i);
        }
        return names;
      },

      // decorate a single rule with property info
      decorateRule: function(rule) {
        if (rule.propertyInfo) {
          return rule.propertyInfo;
        }
        var info = {}, properties = {};
        var hasProperties = this.collectProperties(rule, properties);
        if (hasProperties) {
          info.properties = properties;
          // TODO(sorvell): workaround parser seeing mixins as additional rules
          rule.rules = null;
        }
        info.cssText = this.collectCssText(rule);
        rule.propertyInfo = info;
        return info;
      },

      // collects the custom properties from a rule's cssText
      collectProperties: function(rule, properties) {
        var info = rule.propertyInfo;
        if (info) {
          if (info.properties) {
            Polymer.Base.mixin(properties, info.properties);
            return true;
          }
        } else {
          var m, rx = this.rx.VAR_ASSIGN;
          var cssText = rule.parsedCssText;
          var any;
          while (m = rx.exec(cssText)) {
            // note: group 2 is var, 3 is mixin
            properties[m[1]] = (m[2] || m[3]).trim();
            any = true;
          }
          return any;
        }
      },

      // returns cssText of properties that consume variables/mixins
      collectCssText: function(rule) {
        var customCssText = '';
        var cssText = rule.parsedCssText;
        // NOTE: we support consumption inside mixin assignment
        // but not production, so strip out {...}
        cssText = cssText.replace(this.rx.BRACKETED, '')
          .replace(this.rx.VAR_ASSIGN, '');
        var parts = cssText.split(';');
        for (var i=0, p; i<parts.length; i++) {
          p = parts[i];
          if (p.match(this.rx.MIXIN_MATCH) || p.match(this.rx.VAR_MATCH)) {
            customCssText += p + ';\n';
          }
        }
        return customCssText;
      },

      collectPropertiesInCssText: function(cssText, props) {
        var m;
        while (m = this.rx.VAR_CAPTURE.exec(cssText)) {
          props[m[1]] = true;
        }
      },

      // turns custom properties into realized values.
      reify: function(props) {
        // big perf optimization here: reify only *own* properties
        // since this object has __proto__ of the element's scope properties
        var names = Object.getOwnPropertyNames(props);
        for (var i=0, n; i < names.length; i++) {
          n = names[i];
          props[n] = this.valueForProperty(props[n], props);
        }
      },

      // given a property value, returns the reified value
      // a property value may be:
      // (1) a literal value like: red or 5px;
      // (2) a variable value like: var(--a), var(--a, red), or var(--a, --b);
      // (3) a literal mixin value like { properties }. Each of these properties
      // can have values that are: (a) literal, (b) variables, (c) @apply mixins.
      valueForProperty: function(property, props) {
        // case (1) default
        // case (3) defines a mixin and we have to reify the internals
        if (property) {
          if (property.indexOf(';') >=0) {
            property = this.valueForProperties(property, props);
          } else {
            // case (2) variable
            var self = this;
            var fn = function(all, prefix, value, fallback) {
              var propertyValue = (self.valueForProperty(props[value], props) ||
                (props[fallback] ? 
                self.valueForProperty(props[fallback], props) : 
                fallback));
              return prefix + (propertyValue || '');
            };
            property = property.replace(this.rx.VAR_MATCH, fn);
          }
        }
        return property && property.trim() || '';
      },

      // note: we do not yet support mixin within mixin
      valueForProperties: function(property, props) {
        var parts = property.split(';');
        for (var i=0, p, m; (i<parts.length) && (p=parts[i]); i++) {
          m = p.match(this.rx.MIXIN_MATCH);
          if (m) {
            p = this.valueForProperty(props[m[1]], props);
          } else {
            var pp = p.split(':');
            if (pp[1]) {
              pp[1] = pp[1].trim();
              pp[1] = this.valueForProperty(pp[1], props) || pp[1];
            }
            p = pp.join(':');
          }
          parts[i] = (p && p.lastIndexOf(';') === p.length - 1) ? 
            // strip trailing ;
            p.slice(0, -1) :
            p || '';
        }
        return parts.join(';');
      },

      applyProperties: function(rule, props) {
        var output = '';
        // dynamically added sheets may not be decorated so ensure they are.
        if (!rule.propertyInfo) {
          this.decorateRule(rule);
        }
        if (rule.propertyInfo.cssText) {
          output = this.valueForProperties(rule.propertyInfo.cssText, props);
        }
        rule.cssText = output;
      },

      // Test if the rules in these styles matche the given `element` and if so,
      // collect any custom properties into `props`.
      propertyDataFromStyles: function(styles, element) {
        var props = {}, self = this;
        // generates a unique key for these matches
        var o = [], i = 0;
        styleUtil.forRulesInStyles(styles, function(rule) {
          // TODO(sorvell): we could trim the set of rules at declaration 
          // time to only include ones that have properties
          if (!rule.propertyInfo) {
            self.decorateRule(rule);
          }
          if (element && rule.propertyInfo.properties &&
              matchesSelector.call(element, rule.selector)) {
            self.collectProperties(rule, props);
            // produce numeric key for these matches for lookup
            addToBitMask(i, o);
          }
          i++;
        });
        return {properties: props, key: o};
      },

      // Test if a rule matches scope crteria (* or :root) and if so,
      // collect any custom properties into `props`.
      scopePropertiesFromStyles: function(styles) {
        if (!styles._scopeStyleProperties) {
          styles._scopeStyleProperties = 
            this.selectedPropertiesFromStyles(styles, this.SCOPE_SELECTORS);
        }
        return styles._scopeStyleProperties;
      },

      // Test if a rule matches host crteria (:host) and if so,
      // collect any custom properties into `props`.
      //
      // TODO(sorvell): this should change to collecting properties from any
      // :host(...) and then matching these against self.
      hostPropertiesFromStyles: function(styles) {
        if (!styles._hostStyleProperties) {
          styles._hostStyleProperties = 
            this.selectedPropertiesFromStyles(styles, this.HOST_SELECTORS);
        }
        return styles._hostStyleProperties;
      },

      selectedPropertiesFromStyles: function(styles, selectors) {
        var props = {}, self = this;
        styleUtil.forRulesInStyles(styles, function(rule) {
          if (!rule.propertyInfo) {
            self.decorateRule(rule);
          }
          for (var i=0; i < selectors.length; i++) {
            if (rule.parsedSelector === selectors[i]) {
              self.collectProperties(rule, props);
              return;
            }
          }
        });
        return props;
      },

      transformStyles: function(element, properties, scopeSelector) {
        var self = this;
        var hostRx = new RegExp(this.rx.HOST_PREFIX + element.is + 
          this.rx.HOST_SUFFIX);
        return styleTransformer.elementStyles(element, function(rule) {
          self.applyProperties(rule, properties);
          if (rule.cssText && !nativeShadow) {
            self._scopeSelector(rule, hostRx, element.is, 
              element._scopeCssViaAttr, scopeSelector);
          }
        });
      },

      // Strategy: x scope shim a selector e.g. to scope `.x-foo-42` (via classes):
      // non-host selector: .a.x-foo -> .x-foo-42 .a.x-foo
      // host selector: x-foo.wide -> x-foo.x-foo-42.wide
      _scopeSelector: function(rule, hostRx, is, viaAttr, scopeId) {
        rule.transformedSelector = rule.transformedSelector || rule.selector;
        var selector = rule.transformedSelector;
        var scope = viaAttr ? '[' + styleTransformer.SCOPE_NAME + '~=' + 
          scopeId + ']' : 
          '.' + scopeId;
        var parts = selector.split(',');
        for (var i=0, l=parts.length, p; (i<l) && (p=parts[i]); i++) {
          parts[i] = p.match(hostRx) ?
            p.replace(is, is + scope) :
            scope + ' ' + p;
        }
        rule.selector = parts.join(',');
      },

      applyElementScopeSelector: function(element, selector, old, viaAttr) {
        var c = viaAttr ? element.getAttribute(styleTransformer.SCOPE_NAME) :
          element.className;
        v = old ? c.replace(old, selector) :
          (c ? c + ' ' : '') + this.XSCOPE_NAME + ' ' + selector;
        if (c !== v) {
          if (viaAttr) {
            element.setAttribute(styleTransformer.SCOPE_NAME, v);
          } else {
            element.className = v;
          }
        }
      },

      applyElementStyle: function(element, properties, selector, style) {
        // calculate cssText to apply
        var cssText = style ? style.textContent || '' : 
          this.transformStyles(element, properties, selector);  
        // if shady and we have a cached style that is not style, decrement
        var s = element._customStyle;
        if (s && !nativeShadow && (s !== style)) {
          s._useCount--;
          if (s._useCount <= 0) {
            s.parentNode.removeChild(s);
          }
        }
        // apply styling always under native or if we generated style
        // or the cached style is not in document(!)
        if (nativeShadow || (!style || !style.parentNode)) {
          // update existing style only under native
          if (nativeShadow && element._customStyle) {
            element._customStyle.textContent = cssText;
            style = element._customStyle;
          // otherwise, if we have css to apply, do so
          } else if (cssText) {
            // apply css after the scope style of the element to help with
            // style predence rules.
            style = styleUtil.applyCss(cssText, selector, 
              nativeShadow ? element.root : null, element._scopeStyle);
          }
        }
        // ensure this style is our custom style and increment its use count.
        if (style) {
          style._useCount = style._useCount || 0;
          // increment use count if we changed styles
          if (element._customStyle != style) {
            style._useCount++;
          }
          element._customStyle = style;
        }
        return style;
      },

      rx: {
        VAR_ASSIGN: /(?:^|;\s*)(--[^\:;]*?):\s*?(?:([^;{]*?)|{([^}]*)})(?=;)/gim,
        MIXIN_MATCH: /(?:^|\W+)@apply[\s]*\(([^)]*)\);?/im, 
        // note, this supports:
        // var(--a)
        // var(--a, --b)
        // var(--a, fallback-literal)
        // var(--a, fallback-literal(with-one-nested-parens))
        VAR_MATCH: /(^|\W+)var\([\s]*([^,)]*)[\s]*,?[\s]*((?:[^,)]*)|(?:[^;]*\([^;)]*\)))[\s]*?\)/gim,
        VAR_CAPTURE: /\([\s]*(--[^,\s)]*)(?:,[\s]*(--[^,\s)]*))?(?:\)|,)/gim,
        BRACKETED: /\{[^}]*\}/g,
        HOST_PREFIX: '(?:^|[^.])',
        HOST_SUFFIX: '($|[.:[\\s>+~])'
      },

      HOST_SELECTORS: [':host'],
      SCOPE_SELECTORS: [':root'],
      XSCOPE_NAME: 'x-scope'

    };

    function addToBitMask(n, bits) {
      var o = parseInt(n / 32);
      var v = 1 << (n % 32);
      bits[o] = (bits[o] || 0) | v;
    }

  })();

</script>
